/*
 * Terrain.cpp
 *
 *  Created on: Oct 10, 2012
 *      Author: travis
 */

#ifdef _WIN32 
#include <windows.h> 
#endif

#include "Terrain.h"

namespace Terrain
{

Terrain::Terrain(Water *_water, int size, float step)
{
#ifdef _DEBUG
	std::cout << "\nTERRAIN: Init" << std::endl;
#endif
	STEP = step; //0.1
	SIZE = size;

	std::string pathbuff;

	pathbuff = Engine::fileStructure.texturePath
			+ Engine::mapFileStruct.terrainPath;

	// loading textures
	textures = new TexPool(pathbuff.c_str());
	textures->loadDir();

	tex[0] = textures->getTexIdByName("sand.png");
	tex[1] = textures->getTexIdByName("grass.jpg");
	tex[2] = textures->getTexIdByName("grass.png");
	tex[3] = textures->getTexIdByName("rock.png");
	tex[4] = textures->getTexIdByName("underwater.png");
	tex[5] = textures->getTexIdByName("deformation.png");

	shaderID = Shader::sPool->getProgramID("terrain");
	shadowShaderID = Shader::sPool->getProgramID("terrain_sh");

	water = _water;

	isizeBuffer = 0;

	// create new buffer objects
	createBufferObject(&vertexbuffer, &normalbuffer, &uvbuffer, &elementbuffer);

	//Create the semaphore
	Lock = SDL_CreateSemaphore(1);

	clearTerrain();
}

Terrain::~Terrain(void)
{

	// delete all buffers
	glDeleteBuffers(1, &vertexbuffer);
	glDeleteBuffers(1, &uvbuffer);
	glDeleteBuffers(1, &normalbuffer);
	glDeleteBuffers(1, &elementbuffer);

	free(textures);
}

float Terrain::AVGArea(int x, int y, int xSize, int ySize)
{
	float sum = 0;
	int count = 0;

	for (int xT = x; xT < xSize; xT++)
		for (int yT = y; yT < ySize; yT++)
		{
			sum += points[xT][yT];
			count++;
		}

	return sum / (float) count;
}

void Terrain::createBuildPlace(DataTypes::Cube *cubePlace, glm::vec2 pos)
{
	int x, y, xSize, ySize;

	if (cubePlace != NULL)
	{
		x = (int) ((pos.x - abs(cubePlace->getVertexes()[BOTTOM_LEFT_D].x)) / STEP);
		y = (int) ((pos.y - abs(cubePlace->getVertexes()[BOTTOM_LEFT_D].z)) / STEP);
		xSize = (int) (((cubePlace->getVertexes()[BOTTOM_RIGHT_D].x + pos.x) / STEP)
				+ 1);
		ySize =
				(int) (((pos.y - cubePlace->getVertexes()[TOP_LEFT_D].z) / STEP) + 1);

		// check corner of map
		if (xSize > SIZE)
			xSize = SIZE;
		if (ySize > SIZE)
			ySize = SIZE;

		// get AVG of area
		float AVG = AVGArea(x, y, xSize, ySize);

		gCon::con->print("x %i, y %i, xsize %i, ysize %i, AVG %f", x, y, xSize,
				ySize, AVG);

		// set AVG of area
		for (int xT = x; xT < xSize; xT++)
			for (int yT = y; yT < ySize; yT++)
			{
				points[xT][yT] = AVG;
			}

	}
}

void Terrain::terrainSetup(int size, float step)
{
	STEP = step; //0.1
	SIZE = size;
	clearTerrain();
}

void Terrain::clearTerrain()
{
	std::fill(&points[0][0], &points[0][0] + sizeof(points), 0.0f);

	for (int x = 0; x < MAXSIZE; x++)
		for (int y = 0; y < MAXSIZE; y++)
		{
			normals[x][y] = glm::vec3(0.0, 0.0, 0.0);
		}

	minValue = 0.0;
	maxValue = 0.0;
}

glm::vec2 Terrain::getLocalMinMax(int start_x, int start_y, int size)
{
	glm::vec2 out;

	out.y = get_value(start_x, start_y);
	out.x = get_value(start_x, start_y);

	for (int x = start_x; x < start_x + size; x++)
	{
		for (int y = start_y; y < start_y + size; y++)
		{
			if (out.x > get_value(x, y))
			{
				out.x = get_value(x, y);
			}
			else if (out.y < get_value(x, y))
			{
				out.y = get_value(x, y);
			}
		}
	}

	return out;
}

glm::vec3 Terrain::get_normal(int _x, int _y)
{
	return normals[_x][_y];
}

int Terrain::get_size()
{
	return SIZE;
}

float Terrain::get_step()
{
	return STEP;
}

float Terrain::get_value(int _x, int _y)
{
	return points[_x][_y];
}

GLuint Terrain::getElementbuffer() const
{
	return elementbuffer;
}

void Terrain::setElementbuffer(GLuint elementbuffer)
{
	this->elementbuffer = elementbuffer;
}

GLuint Terrain::getNormalbuffer() const
{
	return normalbuffer;
}

void Terrain::setNormalbuffer(GLuint normalbuffer)
{
	this->normalbuffer = normalbuffer;
}

GLuint Terrain::getUvbuffer() const
{
	return uvbuffer;
}

void Terrain::setUvbuffer(GLuint uvbuffer)
{
	this->uvbuffer = uvbuffer;
}

GLuint Terrain::getVertexbuffer() const
{
	return vertexbuffer;
}

void Terrain::setVertexbuffer(GLuint vertexbuffer)
{
	this->vertexbuffer = vertexbuffer;
}

GLuint Terrain::getIsize() const
{
	return isizeBuffer;
}

void Terrain::setIsize(GLuint isizeBuffer)
{
	this->isizeBuffer = isizeBuffer;
}

void Terrain::normalize()
{
	for (int x = 0; x < SIZE; x++)
		for (int y = 0; y < SIZE; y++)
			if (points[x][y] <= 0)
				points[x][y] = 0.0f;
}

// generovnani terenu
// pomoci diamondSquare funkce
void Terrain::generate_terrain(float iRoughness)
{
	float c1, c2, c3, c4;

	clearTerrain();

	points[0][0] = randInRange();
	points[0][SIZE - 1] = randInRange();
	points[SIZE - 1][SIZE - 1] = randInRange();
	points[SIZE - 1][0] = randInRange();

	c1 = randInRange();
	c2 = randInRange();
	c3 = randInRange();
	c4 = randInRange();

	gRoughness = iRoughness;

	diamondSquare(0.0, 0.0, SIZE, SIZE, c1, c2, c3, c4);

	// vypocet normal
	calcNormals(1, 1, SIZE);

#ifdef _DEBUG
	std::cout << "TERRAIN: Highest point on terrain: " << maxValue << std::endl;
	std::cout << "TERRAIN: Lowest point on terrain: " << minValue << std::endl;
#endif
}

// diamond square algoritmus
// rekurzivni funkce na vypocet pozic bodu
// random generovani terenu
void Terrain::diamondSquare(float x, float y, float width, float height,
		float c1, float c2, float c3, float c4)
{

	float Edge1 = 0.0;
	float Edge2 = 0.0;
	float Edge3 = 0.0;
	float Edge4 = 0.0;
	float Middle = 0.0;

	float newWidth = width / 2;
	float newHeight = height / 2;

	if (width > 1 || height > 1)
	{
		Middle = ((c1 + c2 + c3 + c4) / 4) + Displace(newWidth + newHeight); //Randomly displace the midpoint!
		Edge1 = ((c1 + c2) / 2); //Calculate the edges by averaging the two corners of each edge.
		Edge2 = ((c2 + c3) / 2);
		Edge3 = ((c3 + c4) / 2);
		Edge4 = ((c4 + c1) / 2); //

		diamondSquare(x, y, newWidth, newHeight, c1, Edge1, Middle, Edge4);
		diamondSquare(x + newWidth, y, width - newWidth, newHeight, Edge1, c2,
				Edge2, Middle);
		diamondSquare(x + newWidth, y + newHeight, width - newWidth,
				height - newHeight, Middle, Edge2, c3, Edge3);
		diamondSquare(x, y + newHeight, newWidth, height - newHeight, Edge4, Middle,
				Edge3, c4);

	}
	else
	{
		double c = ((c1 + c2 + c3 + c4) / 4) + 1.0;

		points[(int) (x)][(int) (y)] = c;
		if (width == 2)
		{
			points[(int) (x + 1)][(int) (y)] = c;
		}
		if (height == 2)
		{
			points[(int) (x)][(int) (y + 1)] = c;
		}
		if ((width == 2) && (height == 2))
		{
			points[(int) (x + 1)][(int) (y + 1)] = c;
		}

		if (c > maxValue)
			maxValue = c;
		else if (c < minValue)
			minValue = c;
	}

}

float Terrain::Displace(float SmallSize)
{

	double Max = SmallSize / (SIZE * 2) * gRoughness;
	return (randInRange() - 0.5) * Max;
}

// random value
float Terrain::randInRange()
{
	return (rand() / (float) (RAND_MAX));
}

// vypocet normal terenu
void Terrain::calcNormals(int start_x, int start_y, int size)
{

	glm::vec3 p0, p1, p2, p3, p4;

	float sVec;

	glm::vec3 out;

	for (int x = start_x; x < (start_x + size - 1); x++)
		for (int y = start_y; y < (start_x + size - 1); y++)
		{
			// x,y pozice aktualne zkoumaneho bodu v matici

			// ukladani hodnot potrebnych k vypoctu
			p0.x = 0;
			p0.y = points[x][y]; // vyskova souradnice daneho vektoru
			p0.z = 0;

			p1.x = -STEP;
			p1.y = points[x - 1][y];
			p1.z = 0;

			p2.x = STEP;
			p2.y = points[x + 1][y];
			p2.z = 0;

			p3.x = 0;
			p3.y = points[x][y - 1];
			p3.z = STEP;

			p4.x = 0;
			p4.y = points[x][y + 1];
			p4.z = -STEP;

			out.x = STEP * (p1.y - p2.y);
			out.y = 2 * STEP * STEP;
			out.z = STEP * (p4.y - p3.y);

			sVec = sqrt((out.x * out.x) + (out.y * out.y) + (out.z * out.z));

			out.x = out.x / sVec;
			out.y = out.y / sVec;
			out.z = out.z / sVec;

			normals[x][y] = out;
		}
}

float Terrain::getDif()
{
	float dif = 0.0;

	if (((maxValue > 0.0) && (minValue > 0.0))
			|| ((maxValue < 0.0) && (minValue < 0.0)))
		dif = maxValue - minValue;

	if ((maxValue > 0.0) && (minValue < 0.0))
		dif = maxValue + (minValue * -1);

	if (maxValue == 0.0)
		dif = (minValue * -1);

	if (minValue == 0.0)
		dif = maxValue;

#ifdef _DEBUG
	std::cout << "TERRAIN: Dif max-min is: " << dif << std::endl;
#endif
	return dif;
}

void Terrain::setPoint(int x, int y, float value)
{
	points[x][y] = value;
}

void Terrain::update()
{
	// vypocet normal
	calcNormals(0, 0, SIZE);
	//normalize();
}

void Terrain::setVertex(float x, float z, int a, int b)
{
	glm::vec3 n_buf;
	n_buf = get_normal(a, b);
	buf_normals.push_back(glm::vec3(n_buf.x, n_buf.y, n_buf.z));
	buf_uvs.push_back(glm::vec2(x, z));
	buf_vertices.push_back(glm::vec3(x, get_value(a, b), z));
}

int Terrain::setVectors(int start_x, int start_y, int size)
{
	float _x = (float) start_x * STEP;
	float _z = (float) start_y * STEP;

	if (((start_x + size) > SIZE) || ((start_y + size) > SIZE))
		return 1;

	for (int x = start_x; x < start_x + size; x++)
	{

		for (int y = start_y; y < start_y + size; y++)
		{

			// v1
			setVertex(_x, _z, x, y);

			_x = _x + STEP;

			// v2
			setVertex(_x, _z, x + 1, y);

			_x = _x - STEP;
			_z = _z + STEP;

			// v3
			setVertex(_x, _z, x, y + 1);

			// v3 for next triangle
			setVertex(_x, _z, x, y + 1);

			_x = _x + STEP;
			// v4 for next triangle
			setVertex(_x, _z, x + 1, y + 1);

			_z = _z - STEP;

			// v2 for next triangle
			setVertex(_x, _z, x + 1, y);

			_z = _z + STEP;
			_x = _x - STEP;
		}
		_x = _x + STEP;
		_z = (float) start_y * STEP;

	}
	// create indexing
	// clear old values
	indexed_vertices.clear();
	indexed_uvs.clear();
	indexed_normals.clear();
	indices.clear();

	// indexing
	// external function from special/VBOIndexer.h file
	// so this is't member of class terrain
	indexVBO(buf_vertices, buf_uvs, buf_normals, indices, indexed_vertices,
			indexed_uvs, indexed_normals);

	// clear this values are useless
	buf_vertices.clear();
	buf_uvs.clear();
	buf_normals.clear();

	return 0;
}

void Terrain::createBufferObject(GLuint *v, GLuint *n, GLuint *uv, GLuint *e)
{
	// vertex buffer
	glGenBuffers(1, v);

	// normal buffer
	glGenBuffers(1, n);

	// uv coords buffer
	glGenBuffers(1, uv);

	// indexing, element buffer
	glGenBuffers(1, e);
}

void Terrain::fillBufferObject(GLuint v, GLuint n, GLuint uv, GLuint e)
{
	glBindBuffer(GL_ARRAY_BUFFER, v);
	glBufferData(GL_ARRAY_BUFFER, indexed_vertices.size() * sizeof(glm::vec3),
			&indexed_vertices[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, n);
	glBufferData(GL_ARRAY_BUFFER, indexed_normals.size() * sizeof(glm::vec3),
			&indexed_normals[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, uv);
	glBufferData(GL_ARRAY_BUFFER, indexed_uvs.size() * sizeof(glm::vec2),
			&indexed_uvs[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, e);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned short),
			&indices[0], GL_STATIC_DRAW);

	isizeBuffer = indices.size();

	// not used anymore
	indexed_vertices.clear();
	indexed_uvs.clear();
	indexed_normals.clear();
	indices.clear();
}

void Terrain::draw(GLuint shadowMap, int shaderType)
{
	draw(shadowMap, shaderType, glm::vec3(0.0), vertexbuffer, normalbuffer,
			uvbuffer, elementbuffer, isizeBuffer);
}

void Terrain::draw(GLuint shadowMap, int shaderType, glm::vec3 pos, GLuint v,
		GLuint n, GLuint uv, GLuint e, int isize)
{
	if ((shaderType == SHADER_NORMAL) || (shaderType == SHADER_CLIP_UNDER)
			|| (shaderType == SHADER_CLIP_ABOVE))
	{
		// get matrix
		glm::mat4 ModelMatrix = glm::mat4(1.0f); // model matrix
		//glm::mat4 ViewMatrix = glm::translate(World::world->getView(), pos);
		glm::mat4 ViewMatrix = World::world->getView();
		glm::mat4 MVP = World::world->getProjection() * ViewMatrix * ModelMatrix;

		// get light information
		glm::vec3 LightPos = World::world->getLightPos(); // position of light
		glm::vec3 LightDif = World::world->getLightDif(); // diffuse color of light
		glm::vec3 LightAmb = World::world->getLightAmb(); // ambient color of light
		glm::vec3 LightSpec = World::world->getLightSpe(); // specular color of light

		glm::mat4 depthBiasMVP = World::world->getBiasMVP(), pos;

		// start shader program

		glUseProgram(shaderID);

		// setup shader uniform
		glUniform1i(glGetUniformLocation(shaderID, "ta"), 0);
		glUniform1i(glGetUniformLocation(shaderID, "tb"), 1);
		glUniform1i(glGetUniformLocation(shaderID, "tc"), 2);
		glUniform1i(glGetUniformLocation(shaderID, "td"), 3);
		glUniform1i(glGetUniformLocation(shaderID, "te"), 4);
		glUniform1i(glGetUniformLocation(shaderID, "tf"), 5);
		//glUniform1i(glGetUniformLocation(shaderID, "road"),7);

		glUniform1i(glGetUniformLocation(shaderID, "shadowMap"), 6);

		// model view matrix
		glUniformMatrix4fv(glGetUniformLocation(shaderID, "MVP"), 1, GL_FALSE,
				&MVP[0][0]);
		glUniformMatrix4fv(glGetUniformLocation(shaderID, "M"), 1, GL_FALSE,
				&ModelMatrix[0][0]);
		glUniformMatrix4fv(glGetUniformLocation(shaderID, "V"), 1, GL_FALSE,
				&ViewMatrix[0][0]);
		glUniform3f(glGetUniformLocation(shaderID, "LightPosition_worldspace"),
				LightPos.x, LightPos.y, LightPos.z);
		glUniform3f(glGetUniformLocation(shaderID, "lightAmb"), LightAmb.x,
				LightAmb.y, LightAmb.z);
		glUniform3f(glGetUniformLocation(shaderID, "lightDif"), LightDif.x,
				LightDif.y, LightDif.z);
		glUniform3f(glGetUniformLocation(shaderID, "lightSpec"), LightSpec.x,
				LightSpec.y, LightSpec.z);

		//send bias matrix
		glUniformMatrix4fv(glGetUniformLocation(shaderID, "DepthBiasMVP"), 1,
				GL_FALSE, &depthBiasMVP[0][0]);

		GLfloat h = water->getHigh();
		glUniform1f(glGetUniformLocation(shaderID, "WS"), h);

		if ((shaderType == SHADER_CLIP_UNDER))
			glUniform1i(glGetUniformLocation(shaderID, "clip"), 0); // draw mesh only above water

		if (shaderType == SHADER_CLIP_ABOVE)
			glUniform1i(glGetUniformLocation(shaderID, "clip"), 1); // draw mesh only under water

		if (shaderType == SHADER_NORMAL)
			glUniform1i(glGetUniformLocation(shaderID, "clip"), 2); // draw mesh whole mesh

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, tex[0]);

		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, tex[1]);

		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, tex[2]);

		glActiveTexture(GL_TEXTURE3);
		glBindTexture(GL_TEXTURE_2D, tex[3]);

		glActiveTexture(GL_TEXTURE4);
		glBindTexture(GL_TEXTURE_2D, tex[4]);

		glActiveTexture(GL_TEXTURE5);
		glBindTexture(GL_TEXTURE_2D, tex[5]);

		//glActiveTexture(GL_TEXTURE7);
		//glBindTexture(GL_TEXTURE_2D, tex[6]);

		glActiveTexture(GL_TEXTURE6);
		glBindTexture(GL_TEXTURE_2D, shadowMap);

		// 1rst attribute buffer : vertices
		glEnableVertexAttribArray(0);
		glBindBuffer(GL_ARRAY_BUFFER, v);
		glVertexAttribPointer(0, // attribute
				3, // size
				GL_FLOAT, // type
				GL_FALSE, // normalized?
				0, // stride
				(void*) 0 // array buffer offset
		);

		// 2nd attribute buffer : UVs
		glEnableVertexAttribArray(1);
		glBindBuffer(GL_ARRAY_BUFFER, uv);
		glVertexAttribPointer(1, // attribute
				2, // size
				GL_FLOAT, // type
				GL_FALSE, // normalized?
				0, // stride
				(void*) 0 // array buffer offset
		);

		// normals
		glEnableVertexAttribArray(2);
		glBindBuffer(GL_ARRAY_BUFFER, n);
		glVertexAttribPointer(2, // attribute
				3, // size
				GL_FLOAT, // type
				GL_FALSE, // normalized?
				0, // stride
				(void*) 0 // array buffer offset
		);

		// Index buffer
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, e);

		// Draw the array !
		glDrawElements(GL_TRIANGLES, // mode
				isize, // count
				GL_UNSIGNED_SHORT, // type
				(void*) 0 // element array buffer offset
		);

		glDisableVertexAttribArray(0);
		glDisableVertexAttribArray(1);
		glDisableVertexAttribArray(2);

		// stop shader program
		glUseProgram(0);
	}
	else if (shaderType == SHADER_SHADOW)
	{
		// get matrix
		glm::mat4 ModelMatrix = glm::mat4(1.0f); // model matrix
		//glm::mat4 ViewMatrix = glm::translate(World::world->getView(), pos);
		glm::mat4 ViewMatrix = World::world->getShadowView();
		glm::mat4 MVP = World::world->getShadowProjection() * ViewMatrix
				* ModelMatrix;

		// start shader program

		glUseProgram(shadowShaderID);

		//glUseProgram(shadowShaderID); // use shader

		// model view matrix
		glUniformMatrix4fv(glGetUniformLocation(shadowShaderID, "depthMVP"), 1,
				GL_FALSE, &MVP[0][0]);

		// 1rst attribute buffer : vertices
		glEnableVertexAttribArray(0);
		glBindBuffer(GL_ARRAY_BUFFER, v);
		glVertexAttribPointer(0, // attribute
				3, // size
				GL_FLOAT, // type
				GL_TRUE, // normalized?
				0, // stride
				(void*) 0 // array buffer offset
		);

		// Index buffer
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, e);

		// Draw the array !
		glDrawElements(GL_TRIANGLES, // mode
				isize, // count
				GL_UNSIGNED_SHORT, // type
				(void*) 0 // element array buffer offset
		);

		glDisableVertexAttribArray(0);

		glUseProgram(0); // use shader
	}
}

GLfloat Terrain::getInterYVal(glm::vec2 X)
{

	//###########################################################
	// CRITICAL SECTION !!!
	SDL_SemWait(Lock);

	// X.x = x coordiante
	// X.y = z coordinate in 3D space

	// search for rectangle
	int tx, ty;
	tx = (int) X.x;
	ty = (int) X.y;

	if ((tx >= 0) && (tx <= SIZE - 4) && (ty >= 0) && (ty <= SIZE - 4))
	{

		GLfloat realx = tx * STEP;
		GLfloat realy = ty * STEP;

		DataTypes::Triangle2D tr1(glm::vec2(realx, realy),
				glm::vec2(realx + STEP, realy), glm::vec2(realx, realy + STEP));

		// offset
		// corner vert are not visible
		tx += 1;
		ty += 1;

		DataTypes::Triangle3D tReal;

		if (tr1.pointInTriangle2D(X))
		{
			tReal.setA(glm::vec3(realx, get_value(tx, ty), realy));
			tReal.setB(glm::vec3(realx + STEP, get_value(tx + 1, ty), realy));
			tReal.setC(glm::vec3(realx, get_value(tx, ty + 1), realy + STEP));
		}
		else
		{
			tReal.setA(glm::vec3(realx, get_value(tx, ty + 1), realy + STEP));
			tReal.setB(
					glm::vec3(realx + STEP, get_value(tx + 1, ty + 1), realy + STEP));
			tReal.setC(glm::vec3(realx + STEP, get_value(tx + 1, ty), realy));
		}

		glm::vec4 normal = tReal.NormalOfTriangle();

		SDL_SemPost(Lock);
		//###########################################################

		//YY = t = - (D+A*px+C*pz)/B;
		return -(normal.w + normal.x * X.x + normal.z * X.y) / normal.y;
	}

	SDL_SemPost(Lock);
	//###########################################################

	return 0.0f;
}
}

